package br.com.simtecnologia.access.controll.role;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.com.caelum.vraptor.resource.ResourceMethod;
import br.com.simtecnologia.access.controll.LoginSession;

public abstract class AbstractRoleLogin implements Serializable, LoginSession {
	private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRoleLogin.class);
	private static void debug(String regex, Object ... objects) {
		if (LOGGER.isDebugEnabled())
			LOGGER.debug(String.format(regex, objects));
	}
	
	private static final long serialVersionUID = 4542501133972750683L;
	private final RoleResolver resolver;
	private final Map<String, Set<String>> subRolesMap = new HashMap<String, Set<String>>();
	
	public AbstractRoleLogin(RoleResolver resolver) {
		this.resolver = resolver;
		SubRoles subRoles = getClass().getAnnotation(SubRoles.class);
		if (subRoles == null) {
			debug("@SubRoles annotation not found");
			return;
		}
		
		debug("@SubRoles annotation found");
		
		for(SubRole subRole : subRoles.value()){
			subRolesMap.put(subRole.role(), new HashSet<String>());
			subRolesMap.get(subRole.role()).addAll(Arrays.asList(subRole.sub()));
		}
		
		
		for (Entry<String, Set<String>> entry : subRolesMap.entrySet()) {
			Queue<String> aVisitar = new LinkedList<String>();
			Set<String> jaVisitada = new HashSet<String>();
			
			jaVisitada.add(entry.getKey());
			aVisitar.addAll(entry.getValue());
			while(!aVisitar.isEmpty()) {
				String proxima = aVisitar.poll();
				if (jaVisitada.add(proxima) && subRolesMap.containsKey(proxima))
					aVisitar.addAll(subRolesMap.get(proxima));
			}
			entry.getValue().addAll(jaVisitada);
		}
		
		if (LOGGER.isDebugEnabled()) {
			for (Entry<String, Set<String>> entry : subRolesMap.entrySet()) {
				LOGGER.debug(String.format("Found sub-group for role [%s] including %s",entry.getKey(),entry.getValue()));
			}
		}
	}
	
	private Set<String> roles;
	
	protected void login(String ... roles) {
		if (roles == null || roles.length == 0)
			login((Collection<String>)null);
		else
			login(Arrays.asList(roles));
	}
	
	public Set<String> findSubRoles(String ... roles) {
		return findSubRoles(Arrays.asList(roles));
	}
	
	public Set<String> findSubRoles(Collection<String> roles) {
		if (roles == null)
			return Collections.emptySet();
		else {
			Set<String> result = new HashSet<String>();
			for(String role : roles) {
				if (subRolesMap.containsKey(role))
					result.addAll(subRolesMap.get(role));
				else
					result.add(role);
			}
			return result;
		}
	}
	
	protected void login(Collection<String> roles) {
		if (roles == null || roles.isEmpty()) {
			this.roles = null;
		} else {
			this.roles = findSubRoles(roles);
		}
		
		debug("Login successful for roles %s",this.roles);
	}
	
	/**
	 * Caso deseja criar sua lógica de login, sobresceva esse método e invoque
	 * super.logout(), para apagar as roles.
	 */
	protected void logout() {
		roles = null;
	}
	
	protected Set<String> getRoles() {
		return roles == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(roles);
	}
	
	/**
	 * Deve retornar a role do usuário, caso a {@link #isLogged()} seja true.<br/>
	 * <br/>
	 * Pode retornar <tt>null</tt> caso {@link #isLogged()} seja falso, pois neste
	 * caso não será invocado.
	 * @return
	 */

	@Override
	public boolean hasAccess(ResourceMethod logic) {
		if (!isLogged()) {
			LOGGER.warn("hasAcess called before login");
			return false;
		}
		
		boolean accept = resolver.hasAccess(logic, roles);
//		debug("Access allowed for this session [%s]", this);
		return accept;
	}
}
